{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b1f952bb",
   "metadata": {},
   "source": [
    "### 4.反向传播算法（深度学习基础 Back Portagation）\n",
    "\n",
    "对于机器学习来说，实际上我们想要得到的不是输出的值，而是通过学习训练数据集去得到这个网络里面的权值，这样的话就可以构建起来一个网络，去对其他的输入得到相应的输出。\n",
    "\n",
    "但是，对于一个神经网络的连接方式、层数、每层的节点，虽然也是神经网络的参数，但是他们是通过人为经验提前设定的，我们称之为**超参数（Hyper-Parameters）**，当然对于超参数的学习现在也是可以通过学习得到的，不过是前沿研究，这里不做介绍。\n",
    "\n",
    "还有一个问题，就是我们在前面最小二乘、逻辑回归的应用当中，由于输入输出具有明显的函数关系，所以可以直接写出他的loss function，但是在神经网络当中我们**没有隐层的输出真值**，所以无法直接构造损失函数。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1ea3db2b",
   "metadata": {},
   "source": [
    "对此我们采取如下的方法，首先我们确定我们的目标函数$E_d$\n",
    "\n",
    "$$\n",
    "E_d=\\frac{1}{2}\\sum_{i∈outputs}(t_i-y_i)^2\n",
    "$$\n",
    "\n",
    "其中$t_i$就是原本标签，$y_i$是神经网络训练输出标签，也就是表示了整个模型的误差\n",
    "\n",
    "那么我们就可以用随机梯度下降来对目标函数进行优化，即\n",
    "\n",
    "$$\n",
    "\\omega_{ji}\\leftarrow\\omega_{ji}-\\eta\\frac{\\partial E_d}{\\partial \\omega_{ji}}\n",
    "$$\n",
    "\n",
    "我们发现，要对$E_d$进行优化，同样需要$\\omega_{ji}$的偏导数，观察网络的特征我们可以了解到，某一个权重$\\omega_{ji}$只可以通过影响节点j的输入而对网络产生影响，j的输入$net_j$我们可以表示为\n",
    "\n",
    "![eqn_21_22](images/eqn_21_22.png)\n",
    "\n",
    "而$E_d$又是$net_j$的函数，所以这时我们就可以使用**链式求导法则（划重点，这个是整个反向传播和深度学习基础的精髓之一）**，实现$E_d$对$\\omega_{ji}$的求导\n",
    "\n",
    "![eqn_23_25](images/eqn_23_25.png)\n",
    "\n",
    "**接下来我们就来实现$\\frac{\\partial E_d}{\\partial net_j}$,需要区分输出层和隐层两种情况**\n",
    "\n",
    "![nn3](images/nn3.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "706e199a",
   "metadata": {},
   "source": [
    "### 4.1 对于输出层的权值训练\n",
    "\n",
    "和前面一样，当我们需要$net_j$的偏导数的时候，我们先来观察这一项是如何影响输出$E_d$的，可以看到，对于输出层，$net_j$只能通过影响节点j的输出值来对神经网络的输出产生影响，所以我们继续应用链式法则\n",
    "\n",
    "![eqn_26](images/eqn_26.png)\n",
    "\n",
    "他们可以分别写成\n",
    "\n",
    "![eqn_27_29](images/eqn_27_29.png)\n",
    "![eqn_30_31](images/eqn_30_31.png)\n",
    "\n",
    "带入之后可以得到\n",
    "\n",
    "![eqn_ed_net_j.png](images/eqn_ed_net_j.png)\n",
    "\n",
    "如果令$\\delta_j = - \\frac{\\partial E_d}{\\partial net_j}$，也就是一个节点的误差项$\\delta$是网络误差对这个节点输入的偏导数的相反数。带入上式，得到：\n",
    "\n",
    "![eqn_delta_j.png](images/eqn_delta_j.png)\n",
    "\n",
    "将上述推导带入随机梯度下降公式，得到：\n",
    "\n",
    "$$\n",
    "\\omega_{ji}=\\omega_{ji}+\\eta\\delta_jx_{ji}\n",
    "$$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "345a2397",
   "metadata": {},
   "source": [
    "### 4.2 对于隐层的权值训练\n",
    "\n",
    "首先我们定义节点j的所有**直接**下游节点的集合为$Downstream(j)$，这时的$net_j$只能通过影响$Downstream$来影响$E_d$，所以我们假设$net_k$是节点j的下游节点输入（对比$net_j$是节点j的输入），因为有多个$net_k$所以我们需要应用全微分公式，推导如下\n",
    "\n",
    "![eqn_35_40](images/eqn_35_40.png)\n",
    "\n",
    "因为$\\delta_j = - \\frac{\\partial E_d}{\\partial net_j}$，带入上式得到：\n",
    "\n",
    "![eqn_delta_hidden.png](images/eqn_delta_hidden.png)\n",
    "\n",
    "\n",
    "至此，我们已经推导出了反向传播算法。需要注意的是，我们刚刚推导出的训练规则是根据激活函数是sigmoid函数、平方和误差、全连接网络、随机梯度下降优化算法。如果激活函数不同、误差计算方式不同、网络连接结构不同、优化算法不同，则具体的训练规则也会不一样。但是无论怎样，训练规则的推导方式都是一样的，应用链式求导法则进行推导即可。\n",
    "\n",
    "**显然，计算一个节点的误差项$\\delta$，需要先计算每个与其相连的下一层节点的误差项。这就要求误差项的计算顺序必须是从输出层开始，然后反向依次计算每个隐藏层的误差项，直到与输入层相连的那个隐藏层。这就是反向传播算法的名字的含义。当所有节点的误差项计算完毕后，我们就可以对所有的权重开始更新**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e638f73",
   "metadata": {},
   "source": [
    "### 5.为什么要使用激活函数？\n",
    "\n",
    "解释完神经网络的传播之后，我们返回来再来看为什么神经元里需要激活函数这个问题。从生物学角度来说，神经元必须在激活之后才可以向后传播，下面我们主要从数学角度解释这一问题。\n",
    "\n",
    "比如一个两层的神经网络，使用 A 表示激活函数，那么\n",
    "\n",
    "$$\n",
    "y = w_2 A(w_1 x)\n",
    "$$\n",
    "\n",
    "如果我们不使用激活函数，那么神经网络的结果就是\n",
    "\n",
    "$$\n",
    "y = w_2 (w_1 x) = (w_2 w_1) x = \\bar{w} x\n",
    "$$\n",
    "\n",
    "可以看到，我们将两层神经网络的参数合在一起，用 $\\bar{w}$ 来表示，两层的神经网络其实就变成了一层神经网络，只不过参数变成了新的 $\\bar{w}$，所以如果不使用激活函数，那么不管多少层的神经网络，$y = w_n \\cdots w_2 w_1 x = \\bar{w} x$，就都变成了单层神经网络，所以在每一层我们都必须使用激活函数。\n",
    "\n",
    "**在只有单层神经网络的情况下，我们其实是没有办法取近似出所有的复杂函数的,要拟合任意函数，至少需要两层神经网络，所以为了满足复杂度的需求，必须引入activate function。**\n",
    "\n",
    "当然还有一个问题就是两层足够的情况下为什么要引入多层，主要是为了减少网络的整体参数数量，提高学习效率。（感性地认识就是函数越复杂，那么这一层上的神经元越多，神经元之间连接的参数也会越多，这个时候增加一层反而能提高效率）[reference](https://www.zhihu.com/question/271500921)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "94f1754b",
   "metadata": {},
   "source": [
    "* **常用激活函数（神经网络的激活函数都是非线性的）**\n",
    "\n",
    "$$sigmoid=\\sigma(x) = \\frac{1}{1 + e^{-x}}$$\n",
    "\n",
    "![](images/act-sigmoid.jpg)\n",
    "\n",
    "$$tanh(x) = 2 \\sigma(2x) - 1$$\n",
    "\n",
    "![](images/act-tanh.jpg)\n",
    "\n",
    "$$ReLU(x) = max(0, x)$$\n",
    "\n",
    "![](images/act-relu.jpg)\n",
    "\n",
    "当输入 $x<0$ 时，输出为 $0$，当 $x> 0$ 时，输出为 $x$ 该激活函数使网络更快速地收敛。它不会饱和，即它可以对抗梯度消失问题，至少在正区域（$x> 0$ 时）可以这样，因此神经元至少在一半区域中不会把所有零进行反向传播。由于使用了简单的阈值化（thresholding），ReLU 计算效率很高。\n",
    "\n",
    ">进一步地解释以下这个部分，因为我们可以看到神经网络是具有稀疏性的，比方说下面这个图，对于一个特定的问题我们并不会调用所有的神经元，所以当我们不用某个神经元时，就可以让他输出为0，那么这样一来就可以让整个网络的作出高效的输出，可以对训练好的神经网络进行很好的压缩。\n",
    "![](images/nn-sparse.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f4c7038",
   "metadata": {},
   "source": [
    "* **全连接的神经网络的问题**\n",
    "对于全连接神经网络来说，由于每个神经元之间都有联系，会导致我们的输入数据非常大，假如我们输入一个1000x1000的图片，单输入节点就会有1个G的数据存入，而多层的情况就会有非常多的参数，所以一般全连接会放在多层的最后几层或者输出层，在前面的层我们还要对他的结构进一步优化\n",
    "\n",
    "![](images/bp_summary.jpg)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
